/*
 * Copyright 2010 Gaurav Saxena
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.gwtstructs.gwt.client.widgets.autocompleterTextbox;

import java.util.List;

import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.HasAllFocusHandlers;
import com.google.gwt.event.dom.client.HasAllKeyHandlers;
import com.google.gwt.event.dom.client.HasAllMouseHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.BidiUtils;
import com.google.gwt.i18n.client.HasDirection.Direction;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.RichTextArea;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * Autocompleter Widget wraps a textbox with simultaneous filtering of possible values provided as a List. 
 * Currently only the list of String are supported.   
 * @author Gaurav Saxena
 */
class AutoCompleterRichTextArea extends Composite implements HasText
, HasClickHandlers, HasAllFocusHandlers, HasAllKeyHandlers, HasAllMouseHandlers
{
	private VerticalPanel parentPanel = new VerticalPanel();
	private RichTextArea richTextArea;
	private ScrollPanel suggestionsPanel = new ScrollPanel();
	private VerticalPanel suggestionHolder = new VerticalPanel();
	private int currentHighLightedOption = -1;
	private boolean isComparisonCaseSensitive = false;
	private boolean isComparisonStartsFromBeginning = false;
	private List<String> suggestions;
	public enum PanelPosition {TOP, BOTTOM, LEFT, RIGHT};
	private PanelPosition position;
	private HandlerRegistration mouseDownHandler;
	private boolean showSuggestionsWhenTextBoxEmpty = true;
	private HandlerRegistration textboxKeyDownHandler;
	private HandlerRegistration textboxKeyUpHandler;
	private Command keyUpCallback;
	private Widget textboxContainer;
	private boolean renewWidth = true;

	/**
	 * @param suggestions List of String from which filtering of strings would be done
	 * @param isComparisonCaseSensitive if the String comparison needs to be case sensitive. Default: false
	 * @param isComparisonStartsFromBeginning if the comparison should begin from the beginning of the string. 
	 * Default: false
	 * @param position Position of Suggestion Panel among left, top, right, bottom
	 */
	public AutoCompleterRichTextArea(List<String> suggestions, boolean isComparisonCaseSensitive
			, boolean isComparisonStartsFromBeginning, PanelPosition position)
	{
		this(suggestions, isComparisonCaseSensitive, position);
		this.isComparisonStartsFromBeginning  = isComparisonStartsFromBeginning;
	}
	/**
	 * @param suggestions List of String from which filtering of strings would be done
	 * @param isComparisonCaseSensitive if the String comparison needs to be case sensitive
	 * @param position Position of Suggestion Panel among left, top, right, bottom
	 */
	public AutoCompleterRichTextArea(List<String> suggestions, boolean isComparisonCaseSensitive
			, PanelPosition position)
	{
		this(suggestions, position);
		this.isComparisonCaseSensitive = isComparisonCaseSensitive;
	}
	protected AutoCompleterRichTextArea(RichTextArea richTextArea, final List<String> suggestions, PanelPosition position){
		this(richTextArea, richTextArea, suggestions, position);
	}
	protected AutoCompleterRichTextArea(final RichTextArea richTextArea, Widget textboxContainer, final List<String> suggestions
			, PanelPosition position){
		initWidget(parentPanel);
		this.richTextArea = richTextArea;
		this.textboxContainer = textboxContainer;
		this.position = position;
		suggestionsPanel.setVisible(false);
		suggestionsPanel.setStyleName("autocompleter-suggestionPanel");
		DOM.setStyleAttribute(suggestionsPanel.getElement(), "position", "absolute");
		suggestionsPanel.add(suggestionHolder);
		DOM.setStyleAttribute(richTextArea.getElement(), "position", "relative");
		DOM.setStyleAttribute(suggestionsPanel.getElement(), "zIndex", Integer.MAX_VALUE + "");
		suggestionsPanel.setPixelSize(richTextArea.getOffsetWidth(), 200);
		switch(position)
		{
			case TOP:
				parentPanel.add(suggestionsPanel);
				parentPanel.add(textboxContainer);
				DOM.setStyleAttribute(suggestionsPanel.getElement(), "top"
						, (richTextArea.getElement().getOffsetTop()- 200) + "px");
				break;
			case BOTTOM:
				parentPanel.add(textboxContainer);
				parentPanel.add(suggestionsPanel);
				break;
			case LEFT:
			{
				HorizontalPanel hp = new HorizontalPanel();
				parentPanel.add(hp);
				hp.add(suggestionsPanel);
				hp.add(textboxContainer);
				DOM.setStyleAttribute(suggestionsPanel.getElement(), "left"
						, (richTextArea.getElement().getOffsetLeft()- suggestionsPanel.getOffsetWidth()) + "px");
			}
				break;
			case RIGHT:
			{
				HorizontalPanel hp = new HorizontalPanel();
				parentPanel.add(hp);
				hp.add(textboxContainer);
				hp.add(suggestionsPanel);
			}
				break;
		}
		suggestionHolder.setWidth("100%");
		this.suggestions = suggestions;
		this.textboxKeyUpHandler = addKeyUpHandler();
		richTextArea.addKeyUpHandler(new KeyUpHandler(){
			@Override
			public void onKeyUp(KeyUpEvent event) {
				DOM.releaseCapture(richTextArea.getElement());
				if(!KeyUpEvent.isArrow(event.getNativeKeyCode()) 
					&& event.getNativeKeyCode() != KeyCodes.KEY_ENTER
					&& event.getNativeKeyCode() != KeyCodes.KEY_ESCAPE)
				{
					if(keyUpCallback != null)
						keyUpCallback.execute();
					prepareSuggestions();
				}
			}
		});
		this.textboxKeyDownHandler = addKeyDownHandler();
		mouseDownHandler = addMouseHandlerToTextBox();
		
		richTextArea.addBlurHandler(new BlurHandler(){
			@Override
			public void onBlur(BlurEvent event) {
					Timer t = new Timer(){
					@Override
					public void run() {
						if(isValidBlur())
							suggestionsPanel.setVisible(false);
					}};
					t.schedule(100);
		}});
		DOM.setStyleAttribute(this.getElement(), "zIndex", Integer.toString(Integer.MAX_VALUE));
		setUpSuggestionPanelEvents(suggestionsPanel.getElement(), richTextArea.getElement());
	}
	/**
	 * By Default the string comparison is case insensitive.
	 * @param suggestions List of String from which filtering of strings would be done
	 * @param position Position of Suggestion Panel among left, top, right, bottom
	 */
	public AutoCompleterRichTextArea(final List<String> suggestions, PanelPosition position)
	{
		this(new RichTextArea(), suggestions, position);
	}
	/**
	 * @param suggestions List of String from which filtering of strings would be done
	 */
	public AutoCompleterRichTextArea(final List<String> suggestions)
	{
		this(suggestions, PanelPosition.BOTTOM);
	}
	private HandlerRegistration addMouseHandlerToTextBox() {
		return richTextArea.addMouseDownHandler(new MouseDownHandler(){
			@Override
			public void onMouseDown(MouseDownEvent event) {
				if(!suggestionsPanel.isVisible())
					prepareSuggestions();
		}});
	}
	private native boolean isValidBlur() /*-{
		return window.isValidBlur;
	}-*/;
	private native void setUpSuggestionPanelEvents(Element suggestionPanel, Element richTextArea) /*-{
		window.isValidBlur = true;
    	suggestionPanel.onmousedown = function(){
    		window.isValidBlur = false;
    	};
    	suggestionPanel.onmouseout = function(){
    		richTextArea.focus();
    		window.isValidBlur = true;
    	};
  	}-*/;
	
	private HandlerRegistration addKeyDownHandler() {
		return richTextArea.addKeyDownHandler(new KeyDownHandler(){
			@Override
			public void onKeyDown(KeyDownEvent event) {
				if(event.isUpArrow())
				{
					DOM.setCapture(getRichTextArea().getElement());
					if(!suggestionsPanel.isVisible())
						showSuggestionsPanel();
					else if(currentHighLightedOption > 0)
					{
						unHighlight(currentHighLightedOption);
						hightlight(--currentHighLightedOption);
					}
					else
					{
						hightlight(0);
						currentHighLightedOption = 0;
					}
				}
				else if(event.isDownArrow())
				{
					DOM.setCapture(getRichTextArea().getElement());
					if(!suggestionsPanel.isVisible())
						showSuggestionsPanel();
					else if(currentHighLightedOption < suggestionHolder.getWidgetCount() - 1)
					{
						unHighlight(currentHighLightedOption);
						hightlight(++currentHighLightedOption);
					}
					else
					{
						hightlight(suggestionHolder.getWidgetCount() - 1);
						currentHighLightedOption = suggestionHolder.getWidgetCount() - 1;
					}
				}
			}
			private void showSuggestionsPanel() {
				unHighlight(currentHighLightedOption);
				if(!suggestionsPanel.isVisible())
				{
					currentHighLightedOption = -1;
					prepareSuggestions();
				}
			}
		});
	}
	private HandlerRegistration addKeyUpHandler() {
		return richTextArea.addKeyUpHandler(new KeyUpHandler(){
			@Override
			public void onKeyUp(KeyUpEvent event) 
			{
				if(event.getNativeKeyCode() == KeyCodes.KEY_ENTER)
					setSugestion(suggestions, Integer.valueOf(
						DOM.getElementProperty(suggestionHolder.getWidget(currentHighLightedOption).getElement()
						, "index")));
				else if(event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE)
				{
					suggestionsPanel.setVisible(false);
					//event.stopPropagation();
				}
			}
		});
	}

	private void setSugestion(List<String> suggestions, int suggestionIndex) {
		setTextBoxValue(suggestions, suggestionIndex);
		currentHighLightedOption = 0;
		suggestionsPanel.setVisible(false);
	}
	protected void setTextBoxValue(List<String> suggestions, int suggestionIndex) {
		setText(suggestions.get(suggestionIndex));
	}
	protected void prepareSuggestions() {
		if(!showSuggestionsWhenTextBoxEmpty && getRichTextArea().getText().length() == 0)
		{
			suggestionsPanel.setVisible(false);
			return;
		}
		String currentText = getCurrentText();
		int suggestionsLength = suggestionHolder.getWidgetCount();
		suggestionHolder.clear();
		boolean isMatchFound = false;
		for(int i = 0; i < suggestions.size(); i++)
		{
			int index;
			if(isComparisonCaseSensitive)
				index = getSuggestionFilterKey(i).indexOf(currentText);
			else
				index = getSuggestionFilterKey(i).toLowerCase().indexOf(currentText.toLowerCase());
			if((!isComparisonStartsFromBeginning && index > -1) ||
					(isComparisonStartsFromBeginning && index == 0))
			{
				isMatchFound = true;
				suggestionHolder.add(getSuggestion(suggestions, i, index, currentText));
			}
		}
		if(isMatchFound)
		{
			suggestionsPanel.setVisible(true);
			suggestionsPanel.setHeight(Math.min(200, suggestionHolder.getOffsetHeight()) + "px");
			if(renewWidth)
			{
				suggestionsPanel.setWidth(Math.max(getRichTextArea().getOffsetWidth(), suggestionsPanel.getOffsetWidth()) + "px");
				renewWidth = false;
			}
			/*
			 * in FF - The first time suggestionsPanel is rendered, suggestionHolder's height is calculated 
			 * incorrectly. After setting the height of suggestionPanel, suggestionHolder height is calculated 
			 * correctly
			 */
			if(position == PanelPosition.TOP)
				DOM.setStyleAttribute(suggestionsPanel.getElement(), "top"
						, (getRichTextArea().getElement().getOffsetTop() - suggestionsPanel.getOffsetHeight()) + "px");
			if(position == PanelPosition.LEFT)
				DOM.setStyleAttribute(suggestionsPanel.getElement(), "left"
						, (getRichTextArea().getElement().getOffsetLeft()- suggestionsPanel.getOffsetWidth()) + "px");
			if(suggestionsLength != suggestionHolder.getWidgetCount())
				currentHighLightedOption = 0;
			hightlight(currentHighLightedOption);
		}
		else
			suggestionsPanel.setVisible(false);
		
	}
	private HTML getSuggestion(final List<String> suggestions, final int loopIndex, int index
			, String textBoxValue) {
		final HTML option = getSuggestionHtml(loopIndex, index, textBoxValue);
		option.addMouseOverHandler(new MouseOverHandler(){public void onMouseOver(MouseOverEvent event) {
			unHighlight(currentHighLightedOption);
			currentHighLightedOption = suggestionHolder.getWidgetIndex(option);
			hightlight(currentHighLightedOption);
		}});
		option.addClickHandler(new ClickHandler(){public void onClick(ClickEvent event) {
			setSugestion(suggestions, loopIndex);
		}});
		option.setStyleName("autocomplete-option");
		DOM.setElementProperty(option.getElement(), "index", Integer.toString(loopIndex));
		return option;
	}
	private void unHighlight(int currentHighLightedIndex) {
		if(currentHighLightedIndex > -1 && currentHighLightedIndex < suggestionHolder.getWidgetCount())
		{
			Widget currentHighLightedWidget = suggestionHolder.getWidget(currentHighLightedIndex);
			currentHighLightedWidget.removeStyleName("autocomplete-option-highlight");
			currentHighLightedWidget.addStyleName("autocomplete-option-unhighlight");
		}
	}

	private void hightlight(int currentHighLightedIndex) {
		Widget currentHighLightedWidget;
		if(currentHighLightedIndex < 0)
			currentHighLightedOption = 0;
		else if(currentHighLightedIndex >= suggestionHolder.getWidgetCount())
			currentHighLightedOption = suggestionHolder.getWidgetCount() - 1;
		currentHighLightedWidget = suggestionHolder.getWidget(currentHighLightedOption);
		currentHighLightedWidget.removeStyleName("autocomplete-option-unhighlight");
		currentHighLightedWidget.addStyleName("autocomplete-option-highlight");
		currentHighLightedWidget.getElement().scrollIntoView();
	}

	  public void setDirection(Direction direction) {
	    BidiUtils.setDirectionOnElement(getElement(), direction);
	  }

	  public String getText() {
		  return getRichTextArea().getText();
	  }

	  public String getValue() {
	    return getRichTextArea().getText();
	  }

	  @Override
	  public void onBrowserEvent(Event event) {
	    getRichTextArea().onBrowserEvent(event);
	  }
	  /**
	   * Sets this object's text.  Note that some browsers will manipulate the text
	   * before adding it to the widget.  For example, most browsers will strip all
	   * <code>\r</code> from the text, except IE which will add a <code>\r</code>
	   * before each <code>\n</code>.  Use {@link #getText()} to get the text
	   * directly from the widget.
	   * 
	   * @param text the object's new text
	   */
	  public void setText(String text) {
	    getRichTextArea().setText(text);
	  }

	  public HandlerRegistration addBlurHandler(BlurHandler handler) {
	    return getRichTextArea().addBlurHandler(handler);
	  }

	  public HandlerRegistration addClickHandler(ClickHandler handler) {
	    return getRichTextArea().addClickHandler(handler);
	  }

	  public HandlerRegistration addFocusHandler(FocusHandler handler) {
	    return getRichTextArea().addFocusHandler(handler);
	  }

	/**
	 * UP and DOWN arrow keys are used internally to support suggestions. Exercise caution when adding
     * custom keyup handlers
	 */
	public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
	    return getRichTextArea().addKeyDownHandler(handler);
	  }

	  public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
	    return getRichTextArea().addKeyPressHandler(handler);
	  }
	  
    /**
     * ESCAPE and ENTER keys are used internally to support suggestions. Exercise caution when adding
     * custom keyup handlers
	 */
	public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
	    return getRichTextArea().addKeyUpHandler(handler);
	  }

	  public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
	    return getRichTextArea().addMouseDownHandler(handler);
	  }

	  public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
	    return getRichTextArea().addMouseMoveHandler(handler);
	  }

	  public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
	    return getRichTextArea().addMouseOutHandler(handler);
	  }

	  public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
	    return getRichTextArea().addMouseOverHandler(handler);
	  }

	  public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
	    return getRichTextArea().addMouseUpHandler(handler);
	  }

	  public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
	    return getRichTextArea().addMouseWheelHandler(handler);
	  }

	  /**
	   * Gets the tab index.
	   * 
	   * @return the tab index
	   */
	  public int getTabIndex() {
	    return getRichTextArea().getTabIndex();
	  }

	  /**
	   * Gets whether this widget is enabled.
	   * 
	   * @return <code>true</code> if the widget is enabled
	   */
	  public boolean isEnabled() {
	    return getRichTextArea().isEnabled();
	  }

	  public void setAccessKey(char key) {
	    getRichTextArea().setAccessKey(key);
	  }

	  /**
	   * Sets whether this widget is enabled.
	   * 
	   * @param enabled <code>true</code> to enable the widget, <code>false</code>
	   *          to disable it
	   */
	  public void setEnabled(boolean enabled) {
	    getRichTextArea().setEnabled(enabled);
	  }

	  public void setFocus(boolean focused) {
	   getRichTextArea().setFocus(focused);
	  }

	  public void setTabIndex(int index) {
	    getRichTextArea().setTabIndex(index);
	  }
	/**
	 * @return List of string suggestions
	 */
	@SuppressWarnings("unchecked")
	public List getSuggestions() {
		return suggestions;
	}
	/**
	 * The change in the list takes effect immediately. The list of suggestions change as soon as the method is 
	 * invoked
	 * @param suggestions List of string suggestions
	 */
	@SuppressWarnings("unchecked")
	public void setSuggestions(List suggestions) {
		this.suggestions = suggestions;
		this.renewWidth = true;
		prepareSuggestions();
	}
	/**
	 * Sets whether suggestion panel will open when textbox is clicked. By default, showing suggestions when
	 * text box is clicked is enabled
	 * @param disabled if true, behavior is disabled; behavior is enabled if false 
	 */
	public void setOpenOnClickBehavior(boolean disabled)
	{
		if(disabled)
			mouseDownHandler.removeHandler();
		else
			mouseDownHandler = addMouseHandlerToTextBox();
	}
	/**
	 * Sets whether suggestions will be shown if textbox is empty. By default, showing suggestion when textbox is
	 * empty is enabled 
	 * @param disabled if true, suggestions will not be shown; if false, suggestion will be shown
	 */
	public void setShowSuggestionsWhenTextBoxEmpty(boolean disabled)
	{
		this.showSuggestionsWhenTextBoxEmpty = !disabled;
	}
	/**
	 * Sets whether keyboard handling will work for suggestions. By default, Keyboard handling is enabled
	 * @param disabled if true keyboard shortcuts like up arrow, down arrow, escape and enter will not work, if 
	 * false, these will work 
	 */
	public void setKeyboardHandlingBehavior(boolean disabled)
	{
		if(disabled)
		{
			textboxKeyDownHandler.removeHandler();
			textboxKeyUpHandler.removeHandler();
		}
		else
		{
			textboxKeyDownHandler = addKeyDownHandler();
			textboxKeyUpHandler = addKeyUpHandler();
		}
	}
	/**
	 * Sets the width of the suggestions panel. By default it is as long as the textbox
	 * @param width
	 */
	public void setSuggestionWidth(int width)
	{
		suggestionsPanel.setWidth(width + "px");
	}
	/**
	 * Adds the keyup call back to the textbox 
	 * @param cmd callback
	 */
	public void addKeyUpCallback(Command cmd)
	{
		this.keyUpCallback = cmd;
	}
	/**
	 * Removes the key up callback from the textbox
	 */
	public void removeKeyUpCallback()
	{
		this.keyUpCallback = null;
	}
	protected Widget getTextBoxContainer()
	{
		return textboxContainer;
	}
	protected RichTextArea getRichTextArea()
	{
		return richTextArea;
	}
	protected String getCurrentText() {
		return getText();
	}
	protected String getSuggestionFilterKey(int loopIndex) {
		return suggestions.get(loopIndex);
	}
	protected HTML getSuggestionHtml(int loopIndex, int position, String currentTextValue) {
		return new HTML(suggestions.get(loopIndex).substring(0, position) 
				+ "<b>" + suggestions.get(loopIndex).substring(position, position + currentTextValue.length()) 
				+ "</b>" + suggestions.get(loopIndex).substring(position + currentTextValue.length()
						, suggestions.get(loopIndex).length()));
	}
}